/*
 * Copyright 2015 Realm Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.realm;

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import io.realm.entities.AllTypes;
import io.realm.entities.AllTypesModelModule;
import io.realm.log.RealmLog;
import io.realm.rule.BlockingLooperThread;
import io.realm.services.RemoteProcessService;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;


// This is built for testing multi processes related cases.
// To build a test case, create an InterprocessHandler in your test case. This handler will run in the newly
// created thread's Looper. Remember to call Looper.loop() to start handling messages.
// Pass the first thing you want to run to the constructor which will be posted to the beginning of the message queue.
// And add steps you want to run in the remote process in RemoteProcessService.
// Write the comments of the test case like this:
// A-Z means steps running from remote service process.
// 1-9xx means steps running from the main local process.
// eg.: A. Open a Realm
//      1. Open two Realms
//      B. Open three Realms
//      2. assertTrue("OK, remote process win. You can open more Realms than I do in the main local process", false);
@RunWith(AndroidJUnit4.class)
public class RealmInterprocessTest {

    private Realm testRealm;
    private Messenger remoteMessenger;
    private Messenger localMessenger;
    private CountDownLatch serviceStartLatch;
    private final ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            remoteMessenger = new Messenger(iBinder);
            if (serviceStartLatch != null) {
                serviceStartLatch.countDown();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            if (serviceStartLatch != null && serviceStartLatch.getCount() > 1) {
                serviceStartLatch.countDown();
            }
            serviceStartLatch = null;
        }
    };

    BlockingLooperThread looperThread = new BlockingLooperThread();

    // Helper handler to make it easy to interact with remote service process.
    @SuppressLint("HandlerLeak") // SuppressLint bug, doesn't work
    private class InterprocessHandler extends Handler {
        // Timeout Watchdog. In case the service crashed or expected response is not returned.
        // It is very important to feed the dog after the expected message arrived.
        private final static int timeout = 10_000;
        private volatile boolean isTimeout = true;
        private Runnable timeoutRunnable = new Runnable() {
            @Override
            public void run() {
                if (isTimeout) {
                    assertTrue("Timeout happened", false);
                } else {
                    isTimeout = true;
                    postDelayed(timeoutRunnable, timeout);
                }
            }
        };

        protected void clearTimeoutFlag() {
            isTimeout = false;
        }

        protected void done() {
            Looper.myLooper().quit();
        }

        public InterprocessHandler(Runnable startRunnable) {
            super(Looper.myLooper());
            localMessenger = new Messenger(this);
            // To have the first step from main process run.
            post(startRunnable);
            // Starts watchdog.
            postDelayed(timeoutRunnable, timeout);
        }

        @Override
        public void handleMessage(Message msg) {
            Bundle bundle = msg.getData();
            String error = bundle.getString(RemoteProcessService.BUNDLE_KEY_ERROR);
            if (error != null) {
                // Asserts and shows error from service process.
                assertTrue(error, false);
            }
        }
    }

    @Before
    public void setUp() throws Exception {
        Realm.init(InstrumentationRegistry.getInstrumentation().getTargetContext());
        Realm.deleteRealm(getConfiguration());

        // Starts the testing service.
        serviceStartLatch = new CountDownLatch(1);
        Intent intent = new Intent(getContext(), RemoteProcessService.class);
        getContext().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
        assertTrue(serviceStartLatch.await(TestHelper.SHORT_WAIT_SECS, TimeUnit.SECONDS));
    }

    private RealmConfiguration getConfiguration() {
        return new RealmConfiguration.Builder(getContext()).modules(new AllTypesModelModule()).build();
    }

    @After
    public void tearDown() throws Exception {
        int counter = 10;
        getContext().unbindService(serviceConnection);
        remoteMessenger = null;

        // Kills the remote process.
        ActivityManager.RunningAppProcessInfo info = getRemoteProcessInfo();
        if (info != null) {
            android.os.Process.killProcess(info.pid);
        }
        while (getRemoteProcessInfo() != null) {
            if (counter == 0) {
                assertTrue("The remote service process is still alive.", false);
            }
            Thread.sleep(300);
            counter--;
        }
    }

    // Calls this to trigger the next step of service process.
    private void triggerServiceStep(RemoteProcessService.Step step) {
        Message msg = Message.obtain(null, step.message);
        msg.replyTo = localMessenger;
        try {
            remoteMessenger.send(msg);
        } catch (RemoteException e) {
            assertTrue(false);
        }
    }

    // Returns the service info if it is alive.
    // When this method returns null, it doesn't mean the remote process is not existed. An 'empty' process could
    // be retained by the system to be used next time.
    // Use getRemoteProcessInfo if you want to check the existence of remote process.
    private ActivityManager.RunningServiceInfo getServiceInfo() {
        ActivityManager manager = (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningServiceInfo> serviceInfoList = manager.getRunningServices(Integer.MAX_VALUE);
        for (ActivityManager.RunningServiceInfo service : serviceInfoList) {
            if (RemoteProcessService.class.getName().equals(service.service.getClassName())) {
                return service;
            }
        }
        return null;
    }

    private Context getContext() {
        return InstrumentationRegistry.getInstrumentation().getTargetContext();
    }

    // Gets the remote process info if it is alive.
    private ActivityManager.RunningAppProcessInfo getRemoteProcessInfo() {
        ActivityManager manager = (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> processInfoList = manager.getRunningAppProcesses();
        for (ActivityManager.RunningAppProcessInfo info : processInfoList) {
            if (info.processName.equals(getContext().getPackageName() + ":remote")) {
                return info;
            }
        }

        return null;
    }

    // A. Opens a realm, closes it, then calls Runtime.getRuntime().exit(0).
    // 1. Waits 3 seconds to see if the service process existed.
    @Test
    public void exitProcess() {
        looperThread.runBlocking("testThread", true, new Function0<Unit>() {
            @Override
            public Unit invoke() {
                new InterprocessHandler(new Runnable() {
                    @Override
                    public void run() {
                        // Step A
                        triggerServiceStep(RemoteProcessService.stepExitProcess_A);
                    }
                }) {

                    @SuppressWarnings("ConstantConditions")
                    final int servicePid = getServiceInfo().pid;

                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        if (msg.what == RemoteProcessService.stepExitProcess_A.message) {
                            // Step 1
                            clearTimeoutFlag();
                            try {
                                // Timeout is 5 seconds. 3 (6x500ms) seconds should be enough to quit the process.
                                for (int i = 1; i <= 6; i++) {
                                    // We need to retrieve the service's pid again since the system might restart it automatically.
                                    ActivityManager.RunningAppProcessInfo processInfo = getRemoteProcessInfo();
                                    if (processInfo != null && processInfo.pid == servicePid && i >= 6) {
                                        // The process is still alive.
                                        fail("Process is still alive");
                                    } else if (processInfo == null || processInfo.pid != servicePid) {
                                        // The process is gone.
                                        break;
                                    }
                                    Thread.sleep(500);
                                }
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                                assertTrue(false);
                            }
                            looperThread.testComplete();
                        }
                    }
                };
                return Unit.INSTANCE;
            }
        });
    }

    // 1. Main process creates Realm, write one object.
    // A. Service process opens Realm, check if there is one and only one object.
    @Test
    public void createInitialRealm() throws InterruptedException {
        looperThread.runBlocking("testThread", true, new Function0<Unit>() {
            @Override
            public Unit invoke() {
                new InterprocessHandler(new Runnable() {
                    @Override
                    public void run() {
                        // Step 1
                        testRealm = Realm.getInstance(getConfiguration());
                        looperThread.closeAfterTest(testRealm);
                        assertEquals(0, testRealm.where(AllTypes.class).count());
                        testRealm.beginTransaction();
                        testRealm.createObject(AllTypes.class);
                        testRealm.commitTransaction();

                        // Step A
                        triggerServiceStep(RemoteProcessService.stepCreateInitialRealm_A);
                    }
                }) {

                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        if (msg.what == RemoteProcessService.stepCreateInitialRealm_A.message) {
                            clearTimeoutFlag();
                            looperThread.testComplete();
                        } else {
                            assertTrue(false);
                        }
                    }
                };
                return Unit.INSTANCE;
            }
        });
    }

    @Test
    public void interprocessNotifications() {
        looperThread.runBlocking("testThread", true, new Function0<Unit>() {
            private RealmResults<AllTypes> results;
            private AtomicInteger updateCount = new AtomicInteger(0);

            @Override
            public Unit invoke() {
                new InterprocessHandler(new Runnable() {
                    @Override
                    public void run() {
                        testRealm = Realm.getInstance(getConfiguration());
                        looperThread.closeAfterTest(testRealm);
                        results = testRealm.where(AllTypes.class).findAll();
                        assertEquals(0, results.size());
                        results.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
                            @Override
                            public void onChange(RealmResults<AllTypes> allTypes) {
                                switch(updateCount.incrementAndGet()) {
                                    case 1: {
                                        assertEquals(1, results.size());
                                        break;
                                    }
                                    case 2: {
                                        assertEquals(2, results.size());
                                        looperThread.testComplete();
                                        break;
                                    }
                                }
                            }
                        });
                        triggerServiceStep(RemoteProcessService.stepCreateObjects);
                    }
                }) {
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        if (msg.what == RemoteProcessService.stepCreateObjects.message) {
                            clearTimeoutFlag();
                        } else {
                            assertTrue(false);
                        }
                    }
                };
                return Unit.INSTANCE;
            }
        });
    }
}
